home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
PC Media 20
/
PC MEDIA CD20.iso
/
share
/
prog
/
cursoasm
/
cap7.msg
< prev
next >
Wrap
Text File
|
1993-07-16
|
18KB
|
291 lines
INTRODUCCION AL ASM: USO DE LOS PROGRAMAS ENSAMBLADORES
=======================================================
Como anticipamos en el anterior capítulo, comenzaremos aquí con el uso de
los ensambladores comerciales. Después de algo de teoría, veremos el famoso
'Hello, world' tal y como quedaría en lenguaje ensamblador.
Para entender bien todo el proceso desde el fichero fuente, que suele llevar
la extensión '.ASM', hasta el programa que reside en memoria una vez que el DOS
lo ha cargado, haremos el camino de atrás a delante, es decir, comenzando en el
programa final y acabando en el '.ASM'.
Un programa en memoria, una vez cargado, suele consistir en un bloque de
bytes (desde unas decenas de bytes hasta algunos cientos de kilobytes), en el
que residen tanto el código como los datos y la pila del programa. Ya que la
longitud total puede ser superior a 64 KB, no se puede usar un solo valor de
segmento para acceder a todo el código y a todo los datos del programa. Para
posibilitar el acceso a todo el bloque de programa, este bloque está dividido
en varios bloques distintos, normalmente alineados para comenzar en direcciones
múltiplos de 16d. A estos bloques lógicos (ya que la división no es física), se
les denomina 'segmentos'; no hay que confundir este término con los 'segmentos'
que vimos al ver la arquitectura segmentada de los 80x86: aquellos 'segmentos'
eran siempre de 64K bytes, mientras que estos 'segmentos' pueden tener cual-
quier longitud menor o igual a 64K bytes.
Estos bloques lógicos o segmentos se clasifican en función de lo que contie-
nen. Principalmente, pueden contener código, datos, o constituir espacio para
la pila. Por tanto, se habla de 'segmentos de datos', 'segmentos de código', y
'segmento de pila' (no suele haber más que un segmento de pila).
Durante la ejecución del programa, CS suele contener un valor de forma que
CS:0 sea el primer byte de uno de los segmentos de código, mientras que DS
suele contener un valor de manera que DS:0 sea el primer byte de uno de los
segmentos de datos. SS se suele mantener inalterado a lo largo del programa,
apuntando al segmento de pila.
Para saltar de un punto del programa a otro que resida en otro segmento, es
necesario modificar tanto CS como IP, mientras que si el destino reside en el
mismo segmento que la instrucción de salto basta con modificar IP. De la misma
forma, para acceder a una variable que resida en el segmento de datos apuntado
por DS no es necesario modificar DS, mientras que si la variable reside en otro
segmento de datos es necesario cargar DS o ES para que apunte al principio del
otro segmento antes de acceder a la variable. Como podemos intuir, el acceso
a código y datos 'cercanos' (que residan en el CS y DS actuales) es mucho más
cómodo, además de ligeramente más rápido, que el acceso a código y datos leja-
nos (en otros segmentos que los apuntados por CS y DS).
Más adelante volveremos a este galimatías de segmentos y veremos lo que son
los famosos 'modelos' estándar de memoria. Por ahora, dejemos este tema y siga-
mos.
Esta imagen de memoria la crea el DOS a partir del fichero '.EXE' que el
usuario ha ordenado ejecutar (más adelante veremos las peculiaridades de los
'.COM'). Lo que el DOS hace no es símplemente cargar el '.EXE' tal y como está
en el disco y saltar a la primera dirección del '.EXE', sino que el proceso de
carga es bastante complejo. En realidad, el '.EXE' es el bloque que hemos
visto, compuesto de varios bloques lógicos (llamados segmentos), y precedido de
una cabecera de longitud variable. El estudio detallado de esta cabecera queda
fuera de nuestro ámbito, pero nos interesa saber el contenido aproximado.
Los primeros dos bytes del '.EXE' son la cadena 'MZ', que lo identifican
como '.EXE'. La cabecera indica, además, su propia longitud, un CRC para el
fichero completo (es decir, una suma utilizada para verificar la integridad del
fichero), el punto - dentro del bloque de código y datos - al que hay que sal-
tar (el punto de entrada al programa), los valores iniciales de SS y SP para
apuntar a la pila, y algún detalle más. Pero, además de todo esto, lo más
importante de la cabecera es la llamada 'tabla de reubicación'.
En un sistema MSDOS, los programas deben ser capaces de correr en cualquier
punto de la memoria. Las instrucciones de los procesadores 80x86 son en muchos
casos relativas (es decir, un salto suele ser 'salta 10 bytes más adelante' y
no 'salta a la dirección 310'), con lo que gran parte del código funciona en
cualquier punto de la memoria. Pero existen algunas instrucciones que utilizan
direcciones absolutas, como por ejemplo cargar en un registro la dirección
completa de una variable (offset y segmento). Estas instrucciones tienen que
ser distintas cuando los datos están en la zona baja de la memoria o en la zona
alta. Para solucionar este problema, un '.EXE' lleva en la cabecera una tabla
en la que se indican todas las instrucciones que necesitan ser modificadas al
cargar el programa.
Por tanto, el proceso de carga es el siguiente (realizado por el MSDOS):
- Carga del '.EXE' en dos partes: cabecera y bloque de programa.
- Recorrido de la tabla de reubicación, arreglando cada referencia
marcada en la tabla.
- Inicialización de los registros de segmento, tal y como viene
especificado en la cabecera, y salto al punto de entrada.
Hay que especificar dos cosas en es punto, antes de pasar a los ficheros
'.OBJ': la primera, que todas las referencias de la tabla de reubicación no son
para instrucciones, sino que algunas pueden ser para datos. Esto ocurre en el
caso de que una variable estática, al ser declarada, sea inicializada con un
valor que dependa de la situación el memoria de otra variable. La segunda pun-
tualización es que el MSDOS se encarga de inicializar algunos registros de seg-
mento antes de entrar al programa, pero no todos. CS, evidentemente, apunta al
segmento de código que contien el punto de entrada. SS apunta ya al segmento de
pila. Pero DS, en principio, no apunta a ningún sitio en concreto. Por ello,
las primeras instrucciones de un programa ASM suelen ser para inicializar DS
apuntando al segmento de datos que vayamos a usar.
Un fichero '.EXE' como los que hemos visto lo construye un programa que se
incluye con cualquier compilador o ensamblador, llamado enlazador o LINKer (en
spanglish). Los linkers de Microsoft suelen llamarse LINK.EXE, mientras que los
de Borland se llaman siempre TLINK.EXE.
Un linker toma como entrada un conjunto de uno o varios ficheros llamados
'ficheros objeto', y genera como salida un fichero '.EXE' (opcionalmente, el
TLINK puede generar un '.COM', mientras que con el LINK es necesario convertir
el '.EXE' resultante en '.COM' con la utilidad EXE2BIN). Los ficheros objeto
tienen extensión '.OBJ', y son ficheros generados directamente por los ensam-
bladores y compiladores a partir de nuestros listados con el código fuente.
Un fichero '.OBJ' contiene varios bloques, que son los que se usan luego
para construir los segmentos que van en el '.EXE'. Además de estos bloques, los
ficheros '.OBJ' llevan también algunas referencias que luego deberán ser in-
cluidas en la tabla de reubicación del '.EXE'. Pero lo que diferencia princi-
palmente a un '.OBJ' de un '.EXE' (aparte del formato de fichero), son algo
llamado 'referencias externas' y 'declaraciones públicas'.
Para facilitar el trabajo del programador, los lenguajes actuales permiten
que un programa '.EXE' se construya a partir de código de varios listados
diferentes. Además, cada uno de estos 'módulos' (que así se llaman) puede estar
escrito en un lenguaje diferente, siempre que tengamos un compilador para ese
lenguaje que genere ficheros '.OBJ' estándar. Una de las complicaciones que
esta posibilidad añade, es que es necesario poder acceder desde un módulo a
funciones y variables declaradas en otro módulo. Lo que se hace es permitir a
cualquier módulo hacer declaraciones externas, en las que se indica al compila-
dor que una variable o función está en algún otro módulo con el que luego
linkaremos éste. Así, el '.OBJ' generado incluye precisamente una referencia
externa con el nombre de la función o variable. El módulo que define una varia-
ble o función a la que se quiere acceder desde otro módulo debe declarar a ésta
como 'pública', de manera que el '.OBJ' generado lleve una pequeña indicación
advirtiendo que la variable de ese nombre está en ese módulo, junto con la
dirección de ésta. El linker es el que se encarga de resolver todas estas refe-
rencias, modificar todas las instrucciones que sea posible en este punto y
dejar las que no se puedan arreglar en la tabla de reubicación del '.EXE'.
Otra de las cosas que hace el linker es 'unir' los segmentos de distintos
módulos en uno sólo. Ya que cada segmento del '.OBJ' va acompañado de un nombre
y de algunos datos, si estos coinciden el linker supone que se ha hecho así
intencionadamente y los une en uno sólo, poniendo uno a continuación del otro.
Las reglas que sigue el linker para reconocer los segmentos y actuar en conse-
cuencia son, junto con el eslabón perdido, uno de los misterios más ignotos de
la humanidad. Ahora en serio, las reglas que siguen en LINK y el TLINK para
reconocer el segmento de pila, etc... son ligeramente distintas, pero existe
una manera de hacer todo esto sin preocuparse de los nombres de los segmentos,
y, por ejemplo, linkar un módulo ASM con uno escrito en C y conseguir fundir
los segmentos de código y datos de los dos módulos. Para esto usaremos carácte-
rísticas del MASM 5.0 y superiores, y del TASM 1.0 y superiores, por lo que os
recomiendo que consigáis al menos estas versiones de los ensambladores (con uno
de los dos vale).
Además de los ficheros '.OBJ', un linker puede tambier linkar ficheros
'.LIB'. Un '.LIB' o fichero de librería es un fichero que contiene un conjunto
de ficheros '.OBJ', uno detrás de otro, que por alguna razón se suelen usar
conjuntamente. Además, el '.LIB' lleva un índice hasheado para encontrar rápi-
damente las referencias a símbolos del '.LIB'. Cuando el linker encuentra una
referencia a un símbolo externo, primero lo busca en los '.OBJ' especificados
por el usuario, y en caso de no encontrarlo busca en los índices de las '.LIB'
especificadas. Si lo encuentra, extrae el '.OBJ' de la '.LIB' en el que apare-
ce el símbolo (variable o función) y lo añade al '.EXE' generado. Por ejemplo,
todas las funciones que un compilador de C provee al programador vienen en una
librería (llamada librería estándar). Es bastante sencillo extraer de un '.LIB'
los '.OBJ' que lo componen, aunque la mayoría de los gestores de librería no lo
permiten.
Ahora que ya sabemos cómo son los '.OBJ', el estudio del listado fuente de
programa en ASM resulta bastante sencillo: un listado fuente en lenguaje ensam-
blador es prácticamente una imagen del '.OBJ' que se quiere generar. El
listado está dividido en fragmentos, denominados 'segmentos', que son los blo-
ques que el '.OBJ' lleva para el linker. Estos segmentos pueden ser de código
o de datos. Además, podemos encontrar declaraciones PUBLIC (símbolos que se
exportan en el '.OBJ' para uso y disfrute generalizado de los demás módulos);
podemos también encontrar declaraciones EXTRN (símbolos que se indican en el
'.OBJ' al linker para ser buscados entre los demás módulos).
Veamos ya cómo quedaría en ASM el famoso 'Hello, world!':
=================8<============================8<===========================
PILA SEGMENT STACK 'STACK' ; Abre el segmento de pila
DW 100h DUP (?) ; 200h (512d) bytes de pila
PILA ENDS ; Cierra el segmento de pila
DATOS SEGMENT 'DATA' ; Abre el segmento de datos
Msg DB 'Hello, world!$' ; Mensaje a imprimir
DATOS ENDS ; Cierra el segmento de datos
CODIGO SEGMENT 'CODE' ; Abre el segmento de código
ASSUME CS:CODIGO, DS:DATOS, SS:PILA
Entrada PROC ; Abre el procedimiento 'Entrada'
mov ax,DATOS ; Valor de segmento para 'DATOS'
mov ds,ax ; Para acceder a 'Msg'
mov dx,OFFSET Msg ; Para la int 21h, servicio 9
mov ah,9 ; Especifica servicio9
int 21h ; Invoca servicio 9: imprimir cadena
mov ax,4C00h ; Servicio 4Ch, valor de retorno 0
int 21h ; Invoca servicio 4Ch: retorno al DOS
Entrada ENDP ; Cierra el procedimiento 'Entrada'
CODIGO ENDS ; Cierra el segmento 'CODIGO'
END Entrada ; Fin del programa, punto de entrada 'Entrada'
=================8<============================8<===========================
Los ensambladores más modernos permiten una manera más sencilla de escribir
este programa, usando las directivas de segmento simplificadas, pero creo que
es mejor ver primero la forma 'clásica' y luego la simplificada, con lo que la
comprensión es mucho mayor.
Como vemos, el listado está dividido en tres partes principales, que corres-
ponden directamente a los segmentos del '.OBJ'. El principio de un segmento se
indica con la directiva del ensamblador 'SEGMENT', y el final con la directiva
'ENDS'. Hay que tener en cuenta que estas directivas no son instrucciones en
ASM, sino que indican al ensamblador cómo debe realizar su trabajo, por lo que
no generan código. Al final del listado siempre debe aparecer la directiva END,
que indica el final del listado. Opcionalmente, puede llevar un argumento cuyo
significado veremos más tarde.
El trabajo del ensamblador es básicamente el siguiente: va leyendo líneas
del fichero '.ASM', que serán siempre directivas. Cuando encuentra una línea
con la directiva SEGMENT, prepara un buffer para ir introduciendo en él los
datos que irá leyendo. Inicializa un contador interno a 0, para apuntar al
principio de buffer. Entonces, comienza a procesar las líneas de otra forma
hasta que encuentra un ENDS, que indica el final del segmento. El contenido del
buffer irá al '.OBJ' prácticamente tal y como está. Las líneas interiores a un
segmento (entre un SEGMENT y su ENDS correspondiente) se procesan de otra
forma. Aunque algunas pueden ser directivas, la mayoría están destinadas a ge-
nerar código o datos que irán en el '.OBJ'. Después de almacenar los bytes que
una línea define en el buffer, el contador interno se incrementa para apuntar
a los siguientes bytes libres. Estas líneas pueden ser líneas de código o líne-
as de datos. Si son de código, la sintáxis será la siguiente:
ETIQUETA: INSTRUCCION OPERANDO(S) ;COMENTARIO
Mientras que si son líneas de datos, la sintáxis será la siguiente:
ETIQUETA D<X> DATOS ;COMENTARIO
En esta línea, <X> se sustituye por una letra que indica el tamaño de los
datos (B: bytes, W: palabras, D:dobles palabras, ...).
La etiqueta es un símbolo, en principio único (no puede haber dos etiquetas
iguales en un mismo listado), que el ensamblador almacena en una tabla de sím-
bolos interna junto con el valor del contador interno al procesar la línea.
También se almacena el contexto en el que aparece la etiqueta (si es de datos
o de código, el tamaño de los datos a los que apunta, el segmento al que perte-
nece...). Se utiliza para referenciar desde otro punto una variable por nombre,
en caso de que se trate de una etiqueta de datos, o para saltar a un punto
determinado desde otro lugar del programa.
La instrucción y los operandos son cualquier instrucción válida del 80x86, y
el comentario puede ser cualquier cosa que aclare la instrucción comentada. Los
datos de una línea del segundo tipo pueden ser números separados por comas,
en cuyo caso se sitúan uno detrás del siguiente. Cuando no se quiere iniciali-
zar una variable, se puede poner un símbolo de interrogación ('?'). Si se quie-
re repetir varias veces una secuencia, se puede utilizar la construcción:
n DUP (dato, dato, dato)
Donde 'n' es un número que representa cuántas veces se debe repetir la se-
cuencia.
Estudiemos ahora con detalle el listado HELLO.ASM. Para hacer la prueba, se
puede recortar el listado y salvarlo como HELLO.ASM. Después, podemos ensamblar
con 'TASM HELLO' o con 'MASM HELLO;'. El linkado lo haremos con 'TLINK HELLO' o
'LINK HELLO;', tras lo cual tendremos el HELLO.EXE en el directorio actual. La
sintáxis de la línea de comandos varía ligeramente de una versión a otra del
mismo compilador, por lo que os recomiendo consultar el manual o hacer algunas
pruebas.
Las primeras tres líneas del listado proporcionan una segmento para la pila
del programa. Como vemos, el segmento lleva un nombre que le proporcionamos
nosotros, en este caso el nombre 'PILA'. Este nombre es cualquier identificador
válido en ASM; recordemos que el ensamblador no distingue entre mayúsculas y
minúsculas. Después de la directiva SEGMENT van varios parámetros opcionales.
Ya que normalmente usaremos las directivas de segmento simplificadas, no entra-
remos a detallar estos parámetros. Si alguien tiene mucho interés puede consul-
tar el manual del ensamblador o la ayuda online de éste, en caso de que tenga.
Como se aprecia, se dejan 100h (256d) palabras sin inicializar para la pila.
Esto suele ser suficiente para programas que no usen la pila intensivamente.
Las siguientes tres líneas definen otro segmento, que lleva los datos del
programa. En este caso, la única variable es una cadena con el mensaje a impri-
mir. Cuando tras un DB aparece una cadena entre comillas simples, el ensambla-
dor introduce todos los caracteres, uno detras de otro.
Para no hacer el mensaje demasiado largo, continuaremos el estudio de éste
listado en el siguiente capítulo.
Salut :-)
Jon